# This file is intended to be a final submission. python tester.py Player.py
# should work at all times. If it does not, there is a bug.
# If you're just trying to test a solution, scroll down to the Player
# class.

# This file is intended to be in the same format as a valid solution, so
# that users can edit their solution into Player and then submit just this
# file to the contest. If you see any reason this would not work, please submit
# an Issue to https://github.com/ChadAMiller/hungergames/issues or email me.

# You can see more sample player classes in bots.py


from collections import Counter
from itertools import islice
import random

class BasePlayer(object):
    '''
    Base class so I don't have to repeat bookkeeping stuff.
    Do not edit unless you're working on the simulation.
    '''
    
    def __str__(self):
        try:
            return self.name
        except AttributeError:
            # Fall back on Python default
            return super(BasePlayer, self).__repr__()
    
    def hunt_choices(*args, **kwargs):
        raise NotImplementedError("You must define a strategy!")
        
    def hunt_outcomes(*args, **kwargs):
        pass
        
    def round_end(*args, **kwargs):
        pass
    
class Player(BasePlayer):
    '''
    Your strategy starts here.
    '''

    def __init__(self):
        self.num_decisions = 0
        self.detect_same_rep = {}
        self.offset = 1
        self.reputations = []
        self.own_reputation = []
        self.current_food = 0

    def update(self, current_reputation, player_reputations, current_food):
        self.own_hunts = self.rep_to_hunt(current_reputation)
        self.reputations.append(self.rep_array_to_hunts(player_reputations))
        self.own_reputation.append(self.own_hunts)
        self.current_food = current_food

    def update_num_decisions(self, hunt_decisions):
        self.num_decisions += hunt_decisions

    def rep_to_hunt(self, reputation):
        return round(reputation*self.num_decisions) if reputation > 0 else 0

    def rep_array_to_hunts(self, rep_array):
        return [self.rep_to_hunt(x) for x in rep_array]

    def detect_specific_collusion(self, hunts, prev_round_rep, cur_round_rep, prev_own_rep, cur_own_rep):
        # checks for collusion for a specific value of hunts
        prev_round_rep = Counter(prev_round_rep)
        cur_round_rep = Counter(cur_round_rep)

        predicted_hunts = False
        
        if prev_round_rep[hunts] > 1:
            predicted_hunts = hunts + prev_round_rep[hunts] - 1
            if prev_own_rep == hunts:
                predicted_hunts += 1
                
        if predicted_hunts != False:
            if predicted_hunts in cur_round_rep and cur_round_rep[predicted_hunts] == prev_round_rep[hunts]:
                return predicted_hunts, cur_round_rep[predicted_hunts]

        return -1, -1

    def detect_cons_collusion(self, rounds=4):
        # detects collusion prolonged over several different rounds
        # inputs: reputation array of other players and own reputation array

        def get_hunts_first_round(rounds):
            hunts_first_round = Counter(self.reputations[rounds*-1])
            for x in hunts_first_round.keys():
                yield x

        def check_hunts(hunts, rounds):
            next_collusion = -1
            for x in sorted(range(1, rounds), reverse=True):
                x *= -1
                pos_collusion, hunters = self.detect_specific_collusion(hunts, self.reputations[x-1], self.reputations[x],
                                                               self.own_reputation[x-1], self.own_reputation[x])

                if pos_collusion > -1:
                    hunts = pos_collusion
                else:
                    return False, False

            return hunts, hunters

        
        collusions = {}
        for hunts in get_hunts_first_round(rounds):
            hunts, hunters = check_hunts(hunts, rounds)
            if hunts != False:
                collusions[hunts] = hunters

        return collusions

        

    def handle_collusion(self, collusion, own_hunts):
        def value_of_collusion(hunts, own_hunts, players, players_left, food, n):

            def hunts_needed(hunts, own_hunts, players, players_left,
                             used_hunts = 0, rounds_needed = 0):
                # Calculates how many hunts are needed to reach the desired hunts amount
                # of the colluding players
                # Has to loop over several rounds if the amount of hunts needed is larger
                # than the amount of players left
                do_hunts = hunts - own_hunts
                if do_hunts > 0:
                    if do_hunts > players_left:
                        used_hunts += players_left
                        rounds_needed += 1
                        return hunts_needed(hunts + players - 1, own_hunts + players_left,
                                            players, players_left, used_hunts, rounds_needed)
                    else:
                        used_hunts += do_hunts
                        return used_hunts, rounds_needed

                return 0, 0

            def nth(iterable, n, default=None):
                # Returns the n-th value of an iterable
                return next(islice(iterable, n, None), default)


            def food_after_n_rounds(food, hunts_catch_up, rounds_catch_up, players):
                round_nr = 0
                while True:
                    if round_nr < rounds_catch_up:
                        food -= (hunts_catch_up/rounds_catch_up)*3
                    else:
                        food += players * 2 # Food received after catching up

                    yield food
                    round_nr += 1


            used_hunts, rounds_needed = hunts_needed(hunts, own_hunts, players, players_left)

            if used_hunts*3 > food:
                return 0    # In the worst case (noone hunts with me) I spend used_hunts*3 food on catching up
                            # If m is not reached I will use up used_hunts*3 food and starve if it is higher than my food
            else:
                food_gen = food_after_n_rounds(food, used_hunts, rounds_needed, players)
                return nth(food_gen, n)


        def calc_hunts_needed(hunts, hunters, own_hunts):
            if own_hunts == hunts:
                hunts_needed = hunts + hunters
            else:
                hunts_needed = hunts + hunters - 1
            if hunts_needed > own_hunts:
                extra_hunts = hunts_needed - own_hunts
                return extra_hunts
            else:
                return 0


        if len(collusion) == 1:
            for hunts, hunters in collusion.iteritems():
                if value_of_collusion(hunts, own_hunts, hunters, len(self.reputations[-1]), self.current_food,
                                      len(self.reputations[-1])*len(self.reputations[-1])) > 0:
                    return calc_hunts_needed(hunts, hunters, own_hunts)
                else:
                    return 0
                
        elif len(collusion) > 1:
            highest_value = 0
            highest_values = (0,0)
            for hunts, hunters in collusion.iteritems():
                value = value_of_collusion(hunts, own_hunts, hunters, len(self.reputations[-1]), self.current_food,
                                           len(self.reputations[-1])*len(self.reputations[-1]))
                if value > highest_value:
                    highest_values = (hunts, hunters)
                    highest_value = value

            if highest_value > 0:
                return calc_hunts_needed(highest_values[0], highest_values[1], own_hunts)
            else:
                return 0

        else:
            return 0

    def hunt_choices(
                    self,
                    round_number,
                    current_food,
                    current_reputation,
                    m,
                    player_reputations,
                    ):


        self.update(current_reputation, player_reputations, current_food)
        
        hunts_needed = 0
        if round_number > 4:
            collusions = self.detect_cons_collusion()
            hunts_needed = self.handle_collusion(collusions, self.own_hunts)
            

        hunt_decisions = ['s']*len(player_reputations)

        for x in range(min(len(player_reputations), int(hunts_needed))):
            hunt_decisions[x] = 'h'

    
        self.update_num_decisions(len(player_reputations))
        return hunt_decisions
        

    def hunt_outcomes(self, food_earnings):
        pass
        

    def round_end(self, award, m, number_hunters):
        '''Required function defined in the rules'''
        pass
